/**
 * Copyright 2007 Jason Horman, Pete Aykroyd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.functor;

import java.util.*;

/**
 * A {@link java.util.Map} implementation backed by an array of keys and an array of
 * values. Useful for constructing a map of key value pairs in a single line of
 * code. Example:
 *
 * <pre>
 *  new ArrayMap("key1", "val1", "key2", "val2");
 * </pre>
 */
public class ArrayMap<K,V> extends AbstractMap<K, V> {
	/** The key array. */
	private final K[] keys;
	/** The value array. */
	private final V[] values;

	/**
	 * Construct the array map over the object array.
	 *
	 * @param v Alternating key value pairs.
	 * @throws IllegalArgumentException If the array doesn't have an even number
	 *                                  of arguments that form key/value pairs
	 */
	public ArrayMap(Object ... v) {
		if (v.length % 2 != 0) {
			throw new IllegalArgumentException(
					"Must have an even number of arguments that form key/value pairs");
		}

		int n = v.length / 2;
		keys = (K[]) new Object[n];
		values = (V[]) new Object[n];

		// copy the keys and values into their own arrays
		for (int i = 0; i < n; i++) {
			keys[i] = (K) v[2 * i];
			values[i] = (V) v[2 * i + 1];
		}
	}

	/** Get the backing key array. */
	public K[] getKeys() {
		return keys;
	}

	/** Get the backing value array. */
	public V[] getValues() {
		return values;
	}

	/**
	 * Scans the key array for the key. If found returns the value at the
	 * array position the key was found at.
	 */
	public V get(Object key) {
		for (int i = 0; i < keys.length; i++) {
			if (key.equals(keys[i])) {
				return values[i];
			}
		}
		return null;
	}

	/** Check if a certain key is in the key array. */
	public boolean containsKey(Object key) {
		for (int i = 0; i < keys.length; i++) {
			if (keys[i].equals(key)) {
				return true;
			}
		}
		return false;
	}

	/** Check if a certain value is in the value array. */
	public boolean containsValue(Object value) {
		for (int i = 0; i < values.length; i++) {
			if (values[i].equals(value)) {
				return true;
			}
		}
		return false;
	}

	/** Constructs a list out of the array of values. */
	public Collection<V> values() {
		return Arrays.<V>asList(values);
	}

	/**
	 * Warning, this method must construct a new key set object and a new iterator
	 * object.
	 */
	public Set<K> keySet() {
		return new AbstractSet<K>() {
			public int size() {
				return keys.length;
			}

			public Iterator<K> iterator() {
				return new Iterator() {
					int index = 0;

					public boolean hasNext() {
						return index < keys.length;
					}

					public Object next() {
						return keys[index++];
					}

					public void remove() {
						throw new UnsupportedOperationException();
					}
				};
			}
		};
	}

	/**
	 * Warning, this method must construct new entry set, iterator and {@link java.util.Map.Entry}
	 * objects. It is somewhat expensive.
	 */
	public Set<Map.Entry<K, V>> entrySet() {
		return new AbstractSet<Map.Entry<K, V>>() {
			public int size() {
				return keys.length;
			}

			public Iterator<Map.Entry<K, V>> iterator() {
				return new Iterator<Map.Entry<K, V>>() {
					int index = 0;

					public boolean hasNext() {
						return index < keys.length;
					}

					public Map.Entry<K, V> next() {
						Map.Entry<K, V> entry = new Map.Entry<K, V>() {
							int entryIndex = index;

							public K getKey() {
								return keys[entryIndex];
							}

							public V getValue() {
								return values[entryIndex];
							}

							public V setValue(V value) {
								V oldValue = values[entryIndex];
								values[entryIndex] = value;
								return oldValue;
							}
						};

						index++;
						return entry;
					}

					public void remove() {
						throw new UnsupportedOperationException();
					}
				};
			}
		};
	}

	/** Get the size of the map. */
	public int size() {
		return keys.length;
	}

	/** Is the map empty. */
	public boolean isEmpty() {
		return keys.length == 0;
	}

	/** @throws UnsupportedOperationException	*/
	public V put(K key, V value) {
		throw new UnsupportedOperationException("Can't put data into ArrayMap");
	}

	/** @throws UnsupportedOperationException	*/
	public void clear() {
		throw new UnsupportedOperationException("Can't clear ArrayMap");
	}

	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof ArrayMap)) return false;
		if (!super.equals(o)) return false;
		final ArrayMap arrayMap = (ArrayMap) o;
		if (!Arrays.equals(keys, arrayMap.keys)) return false;
		if (!Arrays.equals(values, arrayMap.values)) return false;
		return true;
	}

	/** Computes the hashcode from the keys and values. Caches the hashcode. */
	public int hashCode() {
		if (hashcode == -1) {
			hashcode = 0;
			for (int i = 0; i < keys.length; i++) {
				hashcode += (keys[i] == null ? 0 : keys[i].hashCode()) ^
						(values[i] == null ? 0 : values[i].hashCode());
			}
		}

		return hashcode;
	}

	/** Cached hashcode. */
	private int hashcode = -1;

}
